#maturity/experimental
The Runtime API lets you interact with SilverBullet programmatically over HTTP: evaluate Lua expressions and run scripts from the command line, scripts, or external tools.
Requests are evaluated via Chrome DevTools Protocol (CDP) in a headless Chrome instance, which does the actual execution so all results reflect the live client state.
note Note The CLI provides a convenient command-line interface for the Runtime API — evaluate Lua, run scripts, open a REPL, and more, without writing raw HTTP requests.
note Note The Runtime API is not available in read-only mode (
SB_READ_ONLY).
The Runtime API is enabled automatically when Chrome or Chromium is detected on your system — no configuration needed.
If Chrome isn't auto-detected, set the path explicitly:
SB_CHROME_PATH=/usr/bin/chromium
To explicitly disable the Runtime API, set SB_RUNTIME_API=0.
Use the -runtime-api Docker image variant, which includes Chromium:
services:
silverbullet:
image: ghcr.io/silverbulletmd/silverbullet:latest-runtime-api
environment:
- SB_USER=me:secret # optional
- SB_AUTH_TOKEN=mytoken # optional, for API auth
volumes:
- myspace:/space
ports:
- "3000:3000"
The -runtime-api image automatically persists the Chrome profile in /space/.chrome-data, avoiding re-indexing on container restarts.
The base Docker image (ghcr.io/silverbulletmd/silverbullet) does not include Chromium and is significantly smaller (~64MB vs ~766MB).
POST /.runtime/lua
The request body is a raw Lua expression as plain text.
curl -d '1 + 1' http://localhost:3000/.runtime/lua
# => {"result":2}
curl -d 'editor.getCurrentPage()' http://localhost:3000/.runtime/lua
# => {"result":"index"}
POST /.runtime/lua_script
The request body is a raw Lua script as plain text. This allows multi-statement scripts with explicit return statements.
curl -d 'local pages = query[from tags.page limit 3 select table.select(_, "name")](from tags.page limit 3 select table.select(_, "name"))
return pages' \
http://localhost:3000/.runtime/lua_script
# => {"result":[{"name":"index"},{"name":"Projects"},{"name":"TODO"}]}
GET /.runtime/screenshot
Captures the current viewport of the headless Chrome instance as a PNG image.
curl -o screenshot.png http://localhost:3000/.runtime/screenshot
GET /.runtime/objects
Returns every tag indexed in the current space as a bare JSON array of strings.
curl -H "Authorization: Bearer $SB_TOKEN" \
http://localhost:3000/.runtime/objects
# => ["header", "item", "page", "task"]
GET /.runtime/objects/{tag}
Returns a JSON array of objects carrying the given tag. Supports filtering, ordering, pagination, and projection via query parameters.
| Query parameter | Meaning |
|---|---|
where[field]=value |
Equality filter. Repeat across fields for AND. |
where[field][op]=value |
Operator filter. Supported ops: eq, ne, gt, gte, lt, lte, in, contains, startsWith. |
order=field or order=field:desc |
Sort key. Repeatable for multi-key sort. |
limit=N |
Default 100, max 1000. |
offset=N |
Default 0. |
select=f1,f2 |
Projection — return only these fields per object. |
debug=1 |
Adds an X-Equivalent-Lua response header showing the Lua query that ran. |
Dotted field paths are supported for nested objects: where[meta.author]=alice.
Tag (and ref) values may contain / — for example SilverBullet's built-in meta/library and meta/template/page tags. Encode the / as %2F in the URL path; the server unescapes one segment for {tag} and one for {ref}. So meta/library becomes meta%2Flibrary, and the page named Daily/2026-05-14 becomes Daily%2F2026-05-14. The sb CLI does this automatically.
The response is a bare JSON array. Paginate by stepping offset in increments of limit until you get fewer than limit items back.
# Unfinished tasks, highest priority first, top 20
curl -H "Authorization: Bearer $SB_TOKEN" \
"http://localhost:3000/.runtime/objects/task?where[done]=false&order=priority:desc&limit=20"
# Pages by a specific author, projecting only name + lastModified
curl -H "Authorization: Bearer $SB_TOKEN" \
"http://localhost:3000/.runtime/objects/page?where[meta.author]=alice&select=name,lastModified"
# Tag containing a slash: encode the slash as %2F
curl -H "Authorization: Bearer $SB_TOKEN" \
"http://localhost:3000/.runtime/objects/meta%2Flibrary"
Query-string values are auto-typed:
42, -3.14 → numbertrue, false → booleannull → nil (matches missing or explicit-null fields)Force a type with a prefix: where[zipCode]=str:01234, where[count][gt]=num:10, where[active]=bool:true.
GET /.runtime/objects/{tag}/{ref}
Returns the object as JSON, or 404 if not found.
curl -H "Authorization: Bearer $SB_TOKEN" \
"http://localhost:3000/.runtime/objects/page/My%20Page"
GET /.runtime/logs
Returns recent console log entries from the headless browser.
| Query parameter | Description |
|---|---|
limit |
Maximum number of entries to return (default: 100, server retains up to 1000) |
since |
Unix millisecond timestamp — only return entries newer than this |
curl http://localhost:3000/.runtime/logs?limit=5
Response: Content-Type: application/json
{
"logs": [
{"level": "log", "text": "[Client] Booting SilverBullet client", "timestamp": 1710000000000},
{"level": "info", "text": "Service worker disabled.", "timestamp": 1710000000050}
]
}
Each entry has:
* level — one of log, info, warn, error, debug
* text — the concatenated console message
* timestamp — unix milliseconds when the entry was captured
The Lua endpoints (/.runtime/lua and /.runtime/lua_script) support an X-Timeout header to control the maximum wait time in seconds (default: 30):
curl -H "X-Timeout: 60" \
-d 'some_long_running_expression()' \
http://localhost:3000/.runtime/lua
All error responses are JSON with Content-Type: application/json and an error key. The objects API additionally returns a code field with a stable machine-readable identifier:
{ "error": "human-readable message", "code": "snake_case_code" }
Status codes used across the Runtime API:
{"error": "Request body is required"} / {"error": "...", "code": "bad_query"}{"error": "Not found", "code": "not_found"}{"error": "<error message>"} / {"code": "internal_error"}{"error": "No headless browser running", "code": "bridge_unavailable"}{"error": "Request timed out", "code": "timeout"}The full set of object-API error codes: bad_query, bad_field, unknown_operator, bad_limit, not_found, bridge_unavailable, timeout, internal_error.
For a kubectl-style command-line client on top of these endpoints, see sb get in the CLI reference.
As documented in Architecture, the vast majority of SilverBullet’s power is implemented in the client. However, there are use cases for programmatically accessing your space with all of SilverBullet (client’s) power.
When the Runtime API is enabled, the server launches a headless (invisible by default) Chrome process upon the first request to an /.remote endpoint. This browser loads the full SilverBullet client, exactly like a regular browser tab, but without a visible window (with some memory optimizations). The client boots normally: it loads all plugs, Lua code and navigates to the index page.
Once ready, the server communicates with the browser directly via Chrome DevTools Protocol (CDP). Because Lua code runs inside a real SilverBullet client, it has access to the full API surface — editor.*, space.*, queries, and everything else available to in-page scripts and widgets. The results reflect live client state.
Set SB_CHROME_SHOW=1 to run Chrome with a visible window — useful for watching what the headless client is doing. Set SB_CHROME_DATA_DIR to a path to persist the Chrome profile between restarts (avoids re-indexing on each restart).
Headless Chrome spawns several processes (browser, network, storage, and renderer). With the full SilverBullet client loaded and indexed, expect roughly 150–200 MB of total RSS across all Chrome processes. The SilverBullet server itself adds ~30 MB on top of this.